매크로를 do-while(0)으로 감싸는 이유 — 여러 문장을 안전한 하나로 묶기

NOTE

여러 문장을 담은 함수형 매크로가 if/else와 만났을 때 깨지는 이유와, 이를 막기 위해 실무에서 거의 반드시 쓰는 do { ... } while (0) 관용구를 정리한다.

0. 왜 필요한가

포인터를 해제하고 곧바로 NULL로 만드는 관례(실무 C 코드 관례와 UB 함정 정리의 B2)를 매번 손으로 치기 귀찮아서 매크로로 묶고 싶다고 하자.

#define FREE_AND_NULL(ptr) free(ptr); ptr = NULL;

이 매크로를 if 문 안에서 중괄호 없이 쓰면 문제가 생긴다.

if (need_cleanup)
    FREE_AND_NULL(p);
else
    log_skip();

1. 전개해보면 보이는 문제

매크로는 텍스트 치환이므로, 위 코드는 전처리 후 실제로 이렇게 된다.

if (need_cleanup)
    free(p); p = NULL;   /* free(p)만 if에 걸리고, p = NULL은 조건과 무관하게 항상 실행된다 */
else                       /* 짝이 되는 if 문이 없어져서 컴파일 에러 */
    log_skip();

if는 다음에 오는 문장 하나만 자신의 몸통으로 인식한다. 매크로가 세미콜론으로 끝나는 문장 두 개(free(ptr); ptr = NULL;)로 펼쳐지면, if는 첫 번째 문장까지만 자기 것으로 가져가고 두 번째 문장은 조건과 무관하게 항상 실행되는 별개의 문장이 되어버린다. 그 결과 else는 짝이 되는 if를 잃어 컴파일 에러가 난다.

2. 중괄호로 감싸는 것만으로는 부족하다

그럼 중괄호로 묶으면 되지 않을까 싶지만, 이번엔 다른 함정이 있다.

#define FREE_AND_NULL(ptr) { free(ptr); ptr = NULL; }
 
if (need_cleanup)
    FREE_AND_NULL(p);   /* 세미콜론까지 붙여서 호출 */
else
    log_skip();

전개하면:

if (need_cleanup)
    { free(p); p = NULL; };   /* 중괄호 블록 뒤에 세미콜론이 남아 "빈 문장"이 추가된다 */
else                            /* 그 빈 문장이 if의 몸통이 되고, else는 다시 짝을 잃는다 */
    log_skip();

호출부에서 관례대로 FREE_AND_NULL(p);처럼 세미콜론을 붙였더니, { ... } 블록 자체가 이미 완전한 문장이라 그 뒤의 세미콜론이 아무 내용도 없는 빈 문장으로 남는다. if는 이 빈 문장까지만 자기 몸통으로 삼고, 진짜 하고 싶었던 { ... } 블록과 else가 서로 끊어진다.

3. 해법 — do { ... } while (0)

#define FREE_AND_NULL(ptr) \
    do { \
        free(ptr); \
        ptr = NULL; \
    } while (0)
if (need_cleanup)
    FREE_AND_NULL(p);   /* do-while(0) 전체가 문법적으로 "문장 하나"이므로 안전하다 */
else
    log_skip();

do { ... } while (0)은 문법적으로 문장 하나이면서, 그 안에 원하는 만큼 문장을 자유롭게 넣을 수 있고, 뒤에 세미콜론을 붙여도 빈 문장이 남지 않는다(do-while 문 자체가 세미콜론으로 끝나는 게 정상이라서 자연스럽게 맞아떨어진다). 조건이 0(항상 거짓)이라 루프는 정확히 한 번만 돌고 끝나며, 최적화가 켜진 컴파일러는 이 조건 검사 자체를 아예 제거하므로 런타임 비용도 없다.

4. 실전 예 — 여러 줄짜리 정리/검증 매크로

이 패턴은 자원 정리, 인자 검증처럼 “여러 줄이 필요하지만 호출부에서는 함수처럼 보이고 싶은” 매크로에 광범위하게 쓰인다.

#define CHECK_NULL_RETURN(ptr, ret) \
    do { \
        if ((ptr) == NULL) { \
            LOG_ERR("null pointer: %s\n", #ptr); \
            return (ret); \
        } \
    } while (0)
 
int process(char *data)
{
    CHECK_NULL_RETURN(data, -1);   /* if/else 어디에 끼워도, 세미콜론을 붙여도 안전 */
    /* ... */
    return 0;
}

5. 자주 하는 실수

  • 매크로 정의 끝에 세미콜론을 넣기 — } while (0);처럼 매크로 정의 자체에 세미콜론을 넣으면, 호출부에서 또 세미콜론을 붙였을 때 이중 세미콜론(빈 문장)이 남는다. 매크로 정의에는 세미콜론을 넣지 않고, 호출하는 쪽이 세미콜론을 붙이게 하는 게 관례다(일반 함수 호출과 똑같은 모양을 유지하기 위함).
  • 값을 반환해야 하는 자리에 이 패턴을 쓰려 하기 — do-while(0)은 문장(statement)이지 값을 만들어내는 표현식(expression)이 아니다. int x = MACRO();처럼 결과값을 받아야 한다면 이 패턴이 아니라 콤마 연산자 기반 표현식 매크로나, 아예 static inline 함수로 만드는 편이 낫다.
  • 매크로 인자에 부작용 있는 표현식을 넣는 문제까지 해결해준다고 착각하기 — do-while(0)은 “여러 문장을 하나로 묶는” 문제만 해결할 뿐, 인자를 여러 번 평가해서 생기는 부작용 중복 문제(MAX(i++, j) 같은 경우)는 별개다. 인자 평가 횟수 문제는 각 인자를 괄호로 감싸는 것과는 다른 차원의 주의가 필요하다.

관련 문서